-module(rtps_domain).

-behaviour(gen_statem).

%%-include_lib("kernel/include/logger.hrl").
-include("rtps.hrl").

%% API
-export([start_link/4,
	 add_participant/3, del_participant/2,
	 whereis/2, info/1]).

%% gen_statem callbacks
-export([init/1, callback_mode/0,
	 running/3,
	 handle_event/4, 
	 terminate/3, code_change/4]).

-record(participant, {id :: non_neg_integer(), guid_prefix :: rtps:guid_prefix()}).
-record(data, {sup :: pid(), domain_id :: rtps:domain_id(),
	       participants :: [#participant{}],
	       opts :: [proplists:property()],
	       reg :: pid() | undefined, participants_sup :: pid() | undefined,
	       transports_sup :: pid() | undefined}).

%%%===================================================================
%%% API
%%%===================================================================

start_link(Sup, Domain_id, Reg, Opts) ->
    gen_statem:start_link({via, rtps_reg, {Reg, Domain_id}}, ?MODULE, [Sup, Domain_id, Opts], []).

-spec add_participant(Pid, Guid_prefix, Opts) -> Result when
      Pid :: pid(),
      Guid_prefix :: rtps:guid_prefix(),
      Opts :: [proplists:property()],
      Result :: supervisor:startchild_ret() | {error, Reason},
      Reason :: term(). 
%% @doc Add a participant to a domain.      
add_participant(Pid, Guid_prefix, Opts) ->
    gen_statem:call(Pid, {add_participant, Guid_prefix, Opts}).
   
-spec del_participant(Pid, Guid_prefix) -> ok when
      Pid :: pid(),
      Guid_prefix :: rtps:guid_prefix().
%% @doc Delete a participant from a domain.      
del_participant(Pid, Guid_prefix) ->
    gen_statem:cast(Pid, {del_participant, Guid_prefix}).

-spec whereis(Pid, Guid_prefix) -> pid() | undefined when
      Pid :: pid(),
      Guid_prefix :: rtps:guid_prefix().
%% @doc This function returns the pid() of the participant identified
%% by Domain_id and Guid_prefix, or undefined if no such participant
%% exists.
whereis(Pid, Guid_prefix) ->
    gen_statem:call(Pid, {whereis, Guid_prefix}).

info(Pid) ->
    gen_statem:call(Pid, info).
    
%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================

%% @private
init([Sup, Domain_id, Opts]) ->
    %% Depending on all possible transport types available within the
    %% domain, the following fragment size is the default fragment
    %% size. A writer may use a different value depending on the
    %% actual transport types it uses. Currently we simply set a
    %% hard coded minimum, but one could also examine the transports
    %% subsystem to find such a value.
    Fragment_size = ?MIN_FRAGMENT_SIZE,
    Opts1 = [{fragment_size, Fragment_size} | Opts],
    {ok, running, #data{sup = Sup, domain_id = Domain_id, participants = [], opts = Opts1}}.

%% @private
callback_mode() ->
    [state_functions, state_enter].

%% @private

running(enter, _Old_state, #data{sup = Sup} = Data) ->
    %% The supervisor is of type one-for-all, so we can assume the pids constants.
    Children = supervisor:which_children(Sup),
    {ok, Reg} = child(Children, registry),
    {ok, Participants_sup} = child(Children, participants),
    {ok, Transports_sup} = child(Children, transports),
    {keep_state, Data#data{reg = Reg, participants_sup = Participants_sup, transports_sup = Transports_sup}};
%% Add a particiapnt to this domain
running({call, From}, {add_participant, Guid_prefix, Extra_opts},
	#data{participants = Participants, opts = Opts, reg = Reg,
	      participants_sup = Participants_sup, transports_sup = Transports_sup} = Data) ->
    case participant(Guid_prefix, Participants) of
	{ok, Id, Participants1} ->
	    %% Because we put the passed in options before the
	    %% domain's options, passed in options will override.
	    Opts1 = [{participant_id, Id}, {transports_sup, Transports_sup} | Extra_opts ++ Opts],
	    Opts2 = case proplists:get_value(builtin_endpoints, Opts) of
			undefined ->
			    [{builtin_endpoints, [subscriptions_reader, subscriptions_writer,
						  publications_reader, publications_writer]} | Opts1];
			_ ->
			    Opts1
		    end,
	    Spec = #{id => Guid_prefix,
		     start => {rtps_participant_sup, start_link, [Guid_prefix, Reg, Opts2]},
		     restart => permanent,
		     type => supervisor},
	    case rtps_ofo_sup:start_child(Participants_sup, Spec) of
		{ok, _Pid} = Reply ->
		    {keep_state, Data#data{participants = Participants1}, [{reply, From, Reply}]};
		Error ->
		    {keep_state_and_data, [{reply, From, Error}]}
	    end;
	Error ->
	    {keep_state_and_data, [{reply, From, Error}]}
    end;
%% Delete a particiapnt including all its endpoints
running(cast, {del_participant, Guid_prefix},
	#data{participants_sup = Participants_sup, participants = Participants} = Data) ->
    rtps_ofo_sup:stop_child(Participants_sup, Guid_prefix),
    Participants1 = lists:keydelete(Guid_prefix, #participant.guid_prefix, Participants),
    {keep_state, Data#data{participants = Participants1}};
running({call, From}, {whereis, Guid_prefix}, #data{reg = Reg}) ->
    Reply = rtps_reg:whereis_name(Reg, Guid_prefix),
    {keep_state_and_data, [{reply, From, Reply}]};
running(Event_type, Event_content, Data) ->
    handle_event(Event_type, Event_content, running, Data).

%% @private
handle_event({call, From}, info, _State, 
	     #data{reg = Reg, domain_id = Domain_id}) ->
    Participants = rtps_reg:which(Reg),
    Reply = [{domain_id, Domain_id},
	     {participants, [Guid_prefix || {Guid_prefix, _Pid} <- Participants]}],
    {keep_state_and_data, [{reply, From, Reply}]};

handle_event(enter,  _Old_state, _New_state, _Data) ->
    keep_state_and_data;

%% TODO: Sink unhadled events. Remove these clauses.
handle_event({call, From} = _Event_type, _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    Reply = ok,
    {keep_state_and_data, [{reply, From, Reply}]};
handle_event(_Event_type,  _Event_content, _State, _Data) ->
    %% ?LOG_DEBUG("Unhandled event: Type ~p, Content ~p", [Event_type,  Event_content]),
    keep_state_and_data.

%% @private
terminate(_Reason, _State, _Data) ->
    ok.

%% @private
code_change(_Old_vsn, State, Data, _Extra) ->
    {ok, State, Data}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% @private
child(Children, Id) ->
    case lists:keyfind(Id, 1, Children) of
	{_Id, Child, _Type, _Modules} when is_pid(Child) ->
	    {ok, Child};
	{_Id, Child, _Type, _Modules} when Child =:= restarting ->
	    {error, Child};
	_ ->
	    {error, notfound}
    end.

%% @private
%% @doc Determine next free participant id to use. It is no problem
%% that the next free participant id, and therefor the port used, can
%% be guessed.
participant(Guid_prefix, Participants) -> 
    case lists:keyfind(Guid_prefix, #participant.guid_prefix, Participants) of
	#participant{id = Id} ->
	    {ok, Id, Participants};
	false ->
	    case next_participant(Participants, 0) of
		{ok, Id} ->
		    Participant = #participant{guid_prefix = Guid_prefix, id = Id},
		    {ok, Id, [Participant | Participants]};
		Error ->
		    Error
	    end
    end.

%% TODO: Make the maximum number of participants configurable and
%% remove the next hard coded limitation: i.e. move the guard on the
%% number of participants to the clause that actually examines the Opts
%% for a maximum.
next_participant(_Participants, ?MAX_PARTICIPANTS) ->
    {error, ?OUT_OF_RESOURCES};
next_participant([#participant{id = Id} | Participants], Id) ->
    next_participant(Participants, Id + 1);
next_participant(_Participants, Id) ->
    {ok, Id}.

